/*
 * Project Name spring-boot-tools
 * File Name MPMessageDecoder
 * Package Name com.huxiaosu.tools.cmpp.codec
 * Create Time 2024/4/18
 * Create by name：liujie
 */
package com.huxiaosu.tools.mina.codec;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.huxiaosu.tools.mina.exception.PushServerException;
import com.huxiaosu.tools.mina.model.BaseMessage;
import com.huxiaosu.tools.mina.model.MsgType;
import com.huxiaosu.tools.mina.model.RequestDataContext;
import com.huxiaosu.tools.mina.utils.Constants;
import com.huxiaosu.tools.mina.utils.WSToolKit;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.future.WriteFuture;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.util.Base64;

import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.StringTokenizer;

/**
 * 自定义解码器
 *
 * @author liujie
 * @date 2024/4/18 23:36
 */
@SuppressWarnings("AlibabaUndefineMagicConstant")
@Slf4j
public class MPMessageDecoder extends CumulativeProtocolDecoder {


    private String charset;
    private final static String END_TAG = "\r\n";
    private final ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;
    private final static String REQUEST_CONTEXT_KEY = "__REQUEST_DATA_CONTEXT";


    public MPMessageDecoder() {
        this.setCharset("UTF-8");
    }

    public MPMessageDecoder(String charset) {
        this.setCharset(charset);
    }

    @Override
    protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out)
            throws Exception {


        if (in.remaining() < 4) {
            return false;
        }
        in.mark();
        log.debug("<session={}>", session);
        WSToolKit.WSSessionState sessionState = WSToolKit.getSessionState(session);
        log.debug("<getSessionState={}>", sessionState);
        if (sessionState != null && sessionState.equals(WSToolKit.WSSessionState.Connected)) {
            if (in.remaining() < 2) {
                return false;
            }

            in.mark().order(this.byteOrder);

            byte fstByte = in.get();

            int opCode = fstByte & 0xf;
            log.debug("<opCode={}>", opCode);
            switch (opCode) {
                case 0x0 -> log.debug("<case 0x0 ");
                case 0x1 ->
                    // 次请求为客户端发送 string 数据
                        log.debug("<case 0x0 > ");
                case 0x2 -> invokerOpCodeTwo(opCode,fstByte,session,in,out);
                case 0x3, 0x4, 0x5, 0x6, 0x7 -> {
                }
                case 0x8 -> closeConnection(session, in);
                default -> closeConnection(session, in);
            }
        } else {
            byte type = in.get();
            log.debug("<type={}>", type);
            // 满足两个条件标识位 websocket
            if (isWs(type) && !isIos(type)) {
                invokerWs(session, in);
            } else if (isWs(type) && isIos(type)) {
                return invokerIos(in, out);
            } else {
                return invokerClient(in,out,type);
            }
        }

        return false;
    }

    /**
     * 处理 websocket
     *
     * @param session 回话信息
     * @param in      消息流
     * @return void
     * @author liujie
     * @date 2024/7/9 11:53
     */
    private void invokerWs(IoSession session, IoBuffer in) {
        try {
            WSToolKit.setSessionState(session);
            String handshakeMessage = in.getString(Constants.CHARSET_DECODER);
            handshakeMessage = "G" + handshakeMessage;
            String[] msgColumns = splitHandshakeMessage(handshakeMessage);
            String requestUri = msgColumns[0];
            String httpVersion = requestUri.substring(requestUri.lastIndexOf("/") + 1);
            String upgradeCol = WSToolKit.getMessageColumnValue(msgColumns, "Upgrade:");
            String connectionCol = WSToolKit.getMessageColumnValue(msgColumns, "Connection:");
            String secWsProtocolCol = WSToolKit.getMessageColumnValue(msgColumns, "Sec-WebSocket-Protocol:");
            String secWskeyCol = WSToolKit.getMessageColumnValue(msgColumns, "Sec-WebSocket-Key:");
            String wsVersionCol = WSToolKit.getMessageColumnValue(msgColumns, "Sec-WebSocket-Version:");
            // 校验重要字段。任何字段不满足条件，都会导致握手失败！
            boolean hasWebsocket = contains(upgradeCol, "websocket");
            boolean hasUpgrade = contains(connectionCol, "upgrade");
            boolean isGetMethod = "GET"
                    .equalsIgnoreCase(WSToolKit.subString(requestUri, 1, " "));

            boolean isSecWsKeyNull = secWskeyCol == null || secWskeyCol.isEmpty();
            boolean isValidVersion = "13".equals(wsVersionCol);
            boolean isValidHttpVer = Float.parseFloat(httpVersion) >= 1.1F;

            if (!hasWebsocket || !hasUpgrade || !isGetMethod) {
                throw new PushServerException("Invalid websocket request!");
            }

            if (isSecWsKeyNull || !isValidVersion || !isValidHttpVer) {
                throw new PushServerException("Invalid websocket request!");
            }

            String secWsAccept = getSecWebsocketAccept(secWskeyCol);
            String response = getResponseData(upgradeCol, connectionCol, secWsAccept,
                    secWsProtocolCol);

            initRequestContext(session, msgColumns);
            in.mark();
            session.write(response);
        } catch (Exception e) {
            log.error("websocket error", e);
        }
    }

     private boolean invokerOpCodeTwo(int opCode, byte fstByte, IoSession session, IoBuffer in, ProtocolDecoderOutput out){
         // websocket 发送 buffer 数据
         log.debug("< case 0x2 opCode={}>", opCode);
         boolean isFinalFrame2 = fstByte < 0;
         boolean isRsvColZero2 = (fstByte & 0x70) == 0;
         if (!isRsvColZero2) {
             closeConnection(session, in);
             return false;
         }
         byte secByte2 = in.get();
         boolean isMasking2 = secByte2 < 0;
         int dataLength2 = 0;
         byte payload2 = (byte) (secByte2 & 0x7f);
         if (payload2 == 126) {
             dataLength2 = in.getUnsignedShort();
         } else if (payload2 == 127) {
             dataLength2 = (int) in.getLong();
         } else {
             dataLength2 = payload2;
         }
         if (in.remaining() < (isMasking2 ? dataLength2 + 4 : dataLength2)) {
             in.reset();
             return false;
         }
         byte[] mask2 = new byte[4];
         byte[] data2 = new byte[dataLength2];
         if (isMasking2) {
             in.get(mask2);
         }
         in.get(data2);

         // 用掩码处理数据。
         for (int i = 0, maskLength2 = mask2.length, looplimit = data2.length; i < looplimit; i++) {
             data2[i] = (byte) (data2[i] ^ mask2[i % maskLength2]);
         }

         // 创建一个对象保存“数据帧的数据类型”。协议规定——对于分片的数据只有第一帧会携带数据类型信息，所以要新建对象保存数据类型，以应对分片。
         RequestDataContext context = (RequestDataContext) session.getAttribute(REQUEST_CONTEXT_KEY);
         if (context == null) {
             context = new RequestDataContext(Constants.CHARSET_DECODER.charset().name());
             context.setMsgType(MsgType.Binary);
             session.setAttribute(REQUEST_CONTEXT_KEY, context);
         }
         context.append(data2);
         if (isFinalFrame2) {
             context = (RequestDataContext) session.removeAttribute(REQUEST_CONTEXT_KEY);

             if (context.getMsgType() == MsgType.Text) {

                 out.write(context.getDataAsString());
             } else {
                 byte[] dataAsArray = context.getDataAsArray();
                 BaseMessage writeMessage = new BaseMessage();
                 writeMessage.setType(dataAsArray[0]);
                 log.debug("<case 0x2 writeMessage type={}>", dataAsArray[0]);
                 byte[] messageLengthByte = new byte[4];
                 System.arraycopy(dataAsArray, 1, messageLengthByte, 0, 4);
                 int messageLength = WSToolKit.bytes2int(messageLengthByte);
                 log.debug("<case 0x2 writeMessage messageLength={}>", messageLength);
                 writeMessage.setBodyLength(messageLength);
                 byte[] messageBytes = new byte[messageLength];

                 System.arraycopy(dataAsArray, 5, messageBytes, 0, messageLength);
                 String messageStr = new String(messageBytes);
                 log.debug("<case 0x2 writeMessage messageBytes={}>", Arrays.toString(messageBytes));
                 log.debug("<case 0x2 writeMessage messageString={}>", messageStr);
                 writeMessage.setBody(messageBytes);
                 out.write(writeMessage);
             }
             return true;
         } else {
             return false;
         }
     }

    /**
     * 处理 IOS 消息
     *
     * @param in  消息流
     * @param out 输出
     * @return boolean
     * @author liujie
     * @date 2024/7/9 11:55
     */
    @SneakyThrows(value = Throwable.class)
    private boolean invokerIos(IoBuffer in, ProtocolDecoderOutput out) {
        String iosMessage = in.getString(Constants.CHARSET_DECODER);
        if (StringUtils.isBlank(iosMessage)) {
            in.mark();
            return false;
        }
        JSONObject registerJson = JSON.parseObject(iosMessage);
        int reType = registerJson.getIntValue("type");
        String body = registerJson.getString("body");
        log.debug("ios request info ,type={},body={}", reType, body);
        BaseMessage baseMessage = new BaseMessage();
        byte[] bytes = body.getBytes(Constants.CHARSET);
        baseMessage.setBody(bytes);
        baseMessage.setBodyLength(bytes.length);
        baseMessage.setType((byte) reType);
        out.write(baseMessage);
        in.mark();
        return in.remaining() > 0;

    }
    /**
     * 处理 客户端 消息
     *
     * @param in  消息流
     * @param out 输出
     * @param type 类型
     * @return boolean
     * @author liujie
     * @date 2024/7/9 11:55
     */
    private boolean invokerClient(IoBuffer in, ProtocolDecoderOutput out, byte type){
        // 获得消息类型
        BaseMessage message = new BaseMessage();
        int bodyLength = in.getInt();
        log.debug("<bodyLength={}>", bodyLength);
        if (bodyLength > in.remaining()) {
            return false;
        }
        message.setBodyLength(bodyLength);
        byte[] body = new byte[bodyLength];
        in.get(body);
        log.debug("<body={}>", Arrays.toString(body));
        message.setBody(body);
        message.setType(type);
        out.write(message);
        in.mark();
        return in.remaining() > 0;
    }

    /**
     * @return the charset
     */
    public String getCharset() {
        return charset;
    }

    /**
     * @param charset the charset to set
     */
    public void setCharset(String charset) {
        this.charset = charset;
    }

    private void initRequestContext(IoSession session, String[] data) {
        session.setAttribute("__SESSION_CONTEXT", data);
    }

    private String[] splitHandshakeMessage(String handshakeMessage) {
        StringTokenizer st = new StringTokenizer(handshakeMessage, END_TAG);
        String[] result = new String[st.countTokens()];

        for (int i = 0; st.hasMoreTokens(); i++) {
            result[i] = st.nextToken();
        }

        return result;
    }

    private boolean contains(String src, String target) {
        if (src == null || src.isEmpty()) {
            return false;
        } else {
            return src.toLowerCase().contains(target);
        }
    }

    private String getSecWebsocketAccept(String secWebsocketkey) throws NoSuchAlgorithmException {

        String srcBuilder = secWebsocketkey +
                "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

        MessageDigest md = MessageDigest.getInstance("SHA-1");

        md.update(srcBuilder.getBytes(Constants.CHARSET_DECODER.charset()));
        byte[] ciphertext = md.digest();

        return new String(Base64.encodeBase64(ciphertext), Constants.CHARSET_DECODER.charset());
    }

    private String getResponseData(String upgrade, String connection, String secWsAccept,
                                   String secWsProtocol) {
        StringBuilder result = new StringBuilder();
        result.append("HTTP/1.1 101 Switching Protocols\r\n");
        result.append("Upgrade:").append(upgrade).append(END_TAG);
        result.append("Connection:").append(connection).append(END_TAG);
        result.append("Sec-WebSocket-Accept:").append(secWsAccept).append(END_TAG);
        if (secWsProtocol != null && !secWsProtocol.isEmpty()) {
            result.append("Sec-WebSocket-Protocol:").append(secWsProtocol).append(END_TAG);
        }
        result.append(END_TAG);
        return result.toString();
    }

    private boolean isWs(byte type) {
        return type != Constants.T101 && type != Constants.T102 && type != Constants.T103;
    }

    private boolean isIos(byte type) {
        // ws
        return type != 71;
    }

    private void closeConnection(IoSession session, IoBuffer buffer) {
        buffer.free();
        session.closeNow();
    }

    @SuppressWarnings("unused")
    private void closeConnection(IoSession session, String errorMsg) {
        session.write(errorMsg).addListener((IoFutureListener<WriteFuture>) future -> future.getSession().closeNow());
    }
}
